package com.bix.util.blizfiles.mpq;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;

/**
 * This encapsulates the functionality and structure of the MPQ Header.
 * 
 * @author squid
 */
public class MPQHeader {
	public	static	int	HEADER_SIZE			= 4 + 4 + 4 + 2 + 1 + 4 + 4 + 4 + 4;
	public	static	int	EXT_HEADER_SIZE = 8 + 2 + 2;
	
	private	MPQVersion	version = MPQVersion.DEFAULT;
	
	//
	// Magic number (MPQ followed by a 0x1A byte).
	//
	private	byte[]			magic = new byte[4];
	
	//
	// Size of the archive header.
	//
	private	int					headerSize;
	
	//
	// Size of the whole archive, including the header. Does not include the
	// strong digital signature, if present. This size is used, among other
	// things, for determining the region to hash in computing the digital
	// signature. This field is deprecated in the Burning Crusade MoPaQ format,
	// and the size of the archive is calculated as the size from the beginning of
	// the archive to the end of the hash table, block table, or extended block
	// table (whichever is largest).
	//
	private	long				archiveSize;
	
	//
	// MoPaQ format version. MPQAPI will not open archives where this is negative.
	// Known versions:
	// 		0000h:	Original format. HeaderSize should be 20h, and large archives
	// 						are not supported.
	// 		0001h:	Burning Crusade format. Header size should be 2Ch, and large
	// 						archives are supported.
	//
	private	short				formatVersion;
	
	//
	// Power of two exponent specifying the number of 512-byte disk sectors in
	// each logical sector in the archive. The size of each logical sector in the
	// archive is 512 * 2^SectorSizeShift. Bugs in the Storm library dictate that
	// this should always be 3 (4096 byte sectors).
	//
	private	byte				sectorSizeShift;
	
	//
	// Offset to the beginning of the hash table, relative to the beginning of
	// the archive.
	//
	private	int					hashTableOffset;
	
	//
	// Offset to the beginning of the block table, relative to the beginning of
	// the archive.
	//
	private	int					blockTableOffset;
	
	//
	// Number of entries in the hash table. Must be a power of two, and must be
	// less than 2^16 for the original MoPaQ format, or less than 2^20 for the
	// Burning Crusade format.
	//
	private	int					hashTableEntries;
	
	//
	// Number of entries in the block table.
	//
	private	int					blockTableEntries;
	
	//
	// Valid only for Burning Crusade and WotLK.
	//
	
	//
	// Offset to the beginning of the extended block table, relative to the
	// beginning of the archive.
	//
	private	long				extendedBlockTableOffset;
	
	//
	// High 16 bits of the hash table offset for large archives.
	//
	private	short				hashTableOffsetHigh;
	
	//
	// High 16 bits of the block table offset for large archives.
	//
	private	short				blockTableOffsetHigh;

	public MPQHeader () {
	}
	
	public void read (ByteBuffer bb) {
		bb.get(magic);
		this.headerSize = bb.getInt();
		this.archiveSize = ((long)bb.getInt() & 0xffffffffL);
		this.formatVersion = bb.getShort();
		this.sectorSizeShift = bb.get();
		this.hashTableOffset = bb.getInt();
		this.blockTableOffset = bb.getInt();
		this.hashTableEntries = bb.getInt();
		this.blockTableEntries = bb.getInt();
		
		if (this.formatVersion == 1) {
			this.version = MPQVersion.WOW_BURNING_CRUSADE;
		}

	}
	
	public void readExtended (ByteBuffer bb) {
		this.extendedBlockTableOffset = bb.getLong();
		this.hashTableOffsetHigh = bb.getShort();
		this.blockTableOffsetHigh = bb.getShort();
	}
	
	public boolean read (FileChannel fc) throws IOException {
		ByteBuffer	bb = ByteBuffer.allocate (HEADER_SIZE);
		
		//
		// Read and process the basic header information.
		//
		if (fc.read (bb) != HEADER_SIZE) {
			return false;
		}
		bb.rewind();
		bb.order(ByteOrder.LITTLE_ENDIAN);
		read (bb);
		
		//
		// Make sure the header just read is valid.  If it is valid, and this is a
		// Burning Crusader version, make sure to read the extended header
		// information.
		//
		if (this.isValid() && this.version == MPQVersion.WOW_BURNING_CRUSADE) {
			ByteBuffer	bbExt = ByteBuffer.allocate (EXT_HEADER_SIZE);
			if (fc.read (bbExt) != EXT_HEADER_SIZE) {
				return false;
			}
			bbExt.rewind ();
			bbExt.order (ByteOrder.LITTLE_ENDIAN);
			readExtended(bbExt);
		}
		return true;
	}

	/**
	 * @return the version
	 */
	public MPQVersion getVersion() {
		return version;
	}

	/**
	 * @return the magic
	 */
	public byte[] getMagic() {
		return magic;
	}

	/**
	 * @return the headerSize
	 */
	public int getHeaderSize() {
		return headerSize;
	}

	/**
	 * @return the archiveSize
	 */
	public long getArchiveSize() {
		return archiveSize;
	}

	/**
	 * @return the formatVersion
	 */
	public short getFormatVersion() {
		return formatVersion;
	}

	/**
	 * @return the sectorSizeShift
	 */
	public byte getSectorSizeShift() {
		return sectorSizeShift;
	}

	/**
	 * @return the hashTableOffset
	 */
	public int getHashTableOffset() {
		return hashTableOffset;
	}

	/**
	 * @return the blockTableOffset
	 */
	public int getBlockTableOffset() {
		return blockTableOffset;
	}

	/**
	 * @return the hashTableEntries
	 */
	public int getHashTableEntries() {
		return hashTableEntries;
	}

	/**
	 * @return the blockTableEntries
	 */
	public int getBlockTableEntries() {
		return blockTableEntries;
	}

	/**
	 * @return the extendedBlockTableOffset
	 */
	public long getExtendedBlockTableOffset() {
		return extendedBlockTableOffset;
	}

	/**
	 * @return the hashTableOffsetHigh
	 */
	public short getHashTableOffsetHigh() {
		return hashTableOffsetHigh;
	}

	/**
	 * @return the blockTableOffsetHigh
	 */
	public short getBlockTableOffsetHigh() {
		return blockTableOffsetHigh;
	}
	
	public boolean isValid () {
		if (this.magic[0] != 'M'		||
				this.magic[1] != 'P'		||
				this.magic[2] != 'Q'		||
				this.magic[3] != 0x1A			) {
			return false;
		}
		return true;
	}
	
	public String toString () {
		String	s = "";
		
		s += "version[" + this.getVersion().toString() + "]";
		s += "headerSize[" + this.getHeaderSize() + "]";
		s += "archiveSize[" + this.getArchiveSize() + "]";
		s += "formatVersion[" + this.getFormatVersion() + "]";
		s += "sectorSizeShift[" + this.getSectorSizeShift() + "]";
		s += "hashTableOffset[" + this.getHashTableOffset() + "]";
		s += "blockTableOffset[" + this.getBlockTableOffset() + "]";
		s += "hashTableEntries[" + this.getHashTableEntries() + "]";
		s += "blockTableEntries[" + this.getBlockTableEntries() + "]";
		s += "extendedBlockTableOffset[" + this.getExtendedBlockTableOffset() + "]";
		s += "hashTableOffsetHigh[" + this.getHashTableOffsetHigh() + "]";
		s += "blockTableOffsetHight[" + this.getBlockTableOffsetHigh() + "]";
		
		return s;
	}
}

